package mux

import (
	"context"
	"encoding/json"
	"encoding/xml"
	"io"
	"log"
	"net/http"
	"path/filepath"
	"reflect"
	"strings"
	"unsafe"

	"gitee.com/go-wena/app/internal/binding"
	"github.com/ghodss/yaml"
)

const (
	HeaderContentDisposition = "Content-Disposition"
	HeaderContentType        = "Content-Type"

	CharsetUTF8 = "charset=utf-8" // CharsetUTF8 utf8 character set

	ApplicationJSON                  = "application/json"
	ApplicationJSONCharsetUTF8       = ApplicationJSON + "; " + CharsetUTF8
	ApplicationJavaScript            = "application/javascript"
	ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8
	ApplicationXML                   = "application/xml"
	ApplicationXMLCharsetUTF8        = ApplicationXML + "; " + CharsetUTF8
	ApplicationYAML                  = "application/yaml"
	ApplicationYAMLCharsetUTF8       = ApplicationYAML + "; " + CharsetUTF8

	TextHTML             = "text/html"
	TextHTMLCharsetUTF8  = TextHTML + "; " + CharsetUTF8
	TextPlain            = "text/plain"
	TextPlainCharsetUTF8 = TextPlain + "; " + CharsetUTF8
	TextYAML            = "text/yaml"
	TextYAMLCharsetUTF8 = TextYAML + "; " + CharsetUTF8
)

type Context struct {
	w http.ResponseWriter
	q *http.Request
}

func (c *Context) Context() context.Context {
	return c.q.Context()
}

func (c *Context) Request() *http.Request {
	return c.q
}

func (c *Context) Writer() http.ResponseWriter {
	return c.w
}

func (c *Context) Bind(out interface{}) error {
	return binding.BindRequest(c.q, out)
}

func (c *Context) Output(out interface{}) {
	accept := strings.ToLower(strings.Split(c.q.Header.Get("Accept"), ",")[0])
	switch accept {
	case "text/json", ApplicationJSON:
		c.JSON(out)
	case "text/xml", ApplicationXML:
		c.XML(out)
	default:
		c.YAML(out)
	}
}

func (c *Context) HeaderSet(key, value string) {
	c.w.Header().Set(key, value)
}

func (c *Context) Bytes(raw []byte, contentType string, code ...int) {
	c.writeBytes(code, contentType, raw)
}

func (c *Context) String(raw string, contentType string, code ...int) {
	c.writeBytes(code, contentType, s2b(raw))
}

// HTML sends an HTTP response with status code.
func (c *Context) HTML(html string, code ...int) {
	c.writeBytes(code, TextHTMLCharsetUTF8, s2b(html))
}

// Plain sends a string response with status code.
func (c *Context) Plain(s string, code ...int) {
	c.writeBytes(code, TextPlainCharsetUTF8, s2b(s))
}

// JSON sends a JSON response with status code.
func (c *Context) JSON(i interface{}, code ...int) {
	if b, err := json.Marshal(i); err == nil {
		c.writeBytes(code, ApplicationJSONCharsetUTF8, b)
	} else {
		c.Error(err.Error(), 500)
	}
}

// JSONIndent sends a JSON response with status code, but it applies prefix and indent to format the output.
func (c *Context) JSONIndent(i interface{}, prefix string, indent string, code ...int) {
	if b, err := json.MarshalIndent(i, prefix, indent); err == nil {
		c.writeBytes(code, ApplicationJSONCharsetUTF8, b)
	} else {
		c.Error(err.Error(), 500)
	}
}

// JSONP sends a JSONP response with status code. It uses `callback` to construct
// the JSONP payload.
func (c *Context) JSONP(callback string, i interface{}, code ...int) {
	if b, err := json.Marshal(i); err == nil {
		c.writeBytes(code, ApplicationJavaScriptCharsetUTF8, s2b(callback+"("), b, s2b(");"))
	} else {
		c.Error(err.Error(), 500)
	}
}

func (c *Context) YAML(i interface{}, code ...int) {
	if b, err := yaml.Marshal(i); err == nil {
		c.writeBytes(code, TextYAMLCharsetUTF8, b)
	} else {
		c.Error(err.Error(), 500)
	}
}

// XML sends an XML response with status code.
func (c *Context) XML(i interface{}, code ...int) {
	if b, err := xml.Marshal(i); err == nil {
		c.writeBytes(code, ApplicationXMLCharsetUTF8, b)
	} else {
		c.Error(err.Error(), 500)
	}
}

// XMLIndent sends an XML response with status code, but it applies prefix and indent to format the output.
func (c *Context) XMLIndent(i interface{}, prefix string, indent string, code ...int) {
	if b, err := xml.MarshalIndent(i, prefix, indent); err == nil {
		c.writeBytes(code, ApplicationXMLCharsetUTF8, b)
	} else {
		c.Error(err.Error(), 500)
	}
}

// File sends a response with the content of the file
func (c *Context) File(r *http.Request, path string) error {
	return c.file(r, path, "", false)
}

// Download the client is prompted to save the file with provided `name`,
// name can be empty, in that case name of the file is used.
func (c *Context) Download(r *http.Request, path string, name string) error {
	return c.file(r, path, name, true)
}

// NoContent sends a response with no body and a status code.
func (c *Context) NoContent() {
	c.w.WriteHeader(http.StatusNoContent)
}

// Error sends a error response with a status code
func (c *Context) Error(message string, code ...int) {
	http.Error(c.w, message, getCode(code, 500))
}

func (c *Context) file(r *http.Request, path, name string, attachment bool) (err error) {
	dir, file := filepath.Split(path)
	if attachment {
		c.HeaderSet(HeaderContentDisposition, "attachment; filename="+name)
	}
	fs := http.Dir(dir)
	f, err := fs.Open(file)
	if err != nil {
		return
	}
	defer closeIt(f)
	fi, _ := f.Stat()
	http.ServeContent(c.w, r, fi.Name(), fi.ModTime(), f)
	return
}

func (c *Context) writeBytes(code []int, contentType string, data ...[]byte) {
	c.w.Header().Set(HeaderContentType, contentType)
	c.w.WriteHeader(getCode(code, 200))
	for _, bytes := range data {
		_, _ = c.w.Write(bytes)
	}
}

func getCode(code []int, def int) int {
	if len(code) > 0 && code[0] > 0 {
		return code[0]
	}
	return def
}

func closeIt(closer io.Closer) {
	if closer != nil {
		if err := closer.Close(); err != nil {
			log.Printf("closeIt: %v\n", err)
		}
	}
}

func s2b(s string) (b []byte) {
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	bh.Data = sh.Data
	bh.Len = sh.Len
	bh.Cap = sh.Len
	return b
}

//
//func b2s(data []byte) string {
//	return *(*string)(unsafe.Pointer(&data))
//}
